Dates and times topic

Timezones & Locales

This is part of the kalender documentation.

Locale

Kalender uses the intl package to localize day and month names. Call initializeDateFormatting() before runApp:

import 'package:intl/date_symbol_data_local.dart';

void main() async {
  await initializeDateFormatting();
  runApp(const MyApp());
}

The function comes from date_symbol_data_local.dart, not from intl.dart. The intl package compiles in the en_US data only, so every other locale needs this call, including en. Without it, kalender throws an error naming the locale that failed and the call to add.

KalenderView has a locale property that controls day/month name formatting. It takes a Locale, and Localizations.localeOf(context) gives you the app's.

KalenderView(
  locale: const Locale('af', 'ZA'),
  eventsController: eventsController,
  kalenderController: kalenderController,
  viewConfiguration: viewConfiguration,
)

Day and month names come from intl. The overlay button that stands in for events that do not fit is labelled with a plus sign and the count, +3, with the number formatted for the calendar's locale, so it needs no translation. The week number's tooltip is the one string that still defaults to English:

MaterialApp(
  theme: ThemeData(
    extensions: [
      KalenderThemeData(
        weekNumberStyle: WeekNumberStyle(tooltip: 'Weeknummer'),
      ),
    ],
  ),
)

It can be set on a single KalenderView by wrapping it in a KalenderTheme, or once for the whole app through KalenderThemeData.

Custom text

Apart from the week number tooltip, every string the calendar writes can be replaced with a string builder on the matching *Components class. Each one receives the BuildContext, so it can read the calendar's own locale with context.kalenderLocale, which is not necessarily the app's locale. intl takes a string, so pass toLanguageTag():

import 'package:intl/intl.dart';

KalenderView(
  locale: const Locale('af', 'ZA'),
  eventsController: eventsController,
  kalenderController: kalenderController,
  viewConfiguration: viewConfiguration,
  components: KalenderComponents(
    multiDayComponents: MultiDayComponents(
      headerComponents: MultiDayHeaderComponents(
        dayHeaderStringBuilder: (context, date) => DateFormat.E(context.kalenderLocale?.toLanguageTag()).format(date),
      ),
    ),
    overlayBuilders: OverlayBuilders(
      multiDayPortalOverlayButtonStringBuilder: (context, n) => '$n meer',
    ),
  ),
)

The builders are dayHeaderStringBuilder and dayHeaderNumberStringBuilder on MultiDayHeaderComponents, timelineStringBuilder on MultiDayBodyComponents, monthDayHeaderStringBuilder on MonthBodyComponents, weekDayHeaderStringBuilder on MonthHeaderComponents, leadingDateStringBuilder on ScheduleComponents, and multiDayPortalOverlayButtonStringBuilder on OverlayBuilders.

The times down the side of a multi-day view are the one case where the default does not always come from the calendar's locale. MaterialLocalizations formats them where the app installs them, so they follow the device's 12-hour or 24-hour setting. Without those localizations they fall back to intl against the calendar's locale. Fix the format with timelineStringBuilder:

MultiDayBodyComponents(
  timelineStringBuilder: (context, time) =>
      '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}',
)

Location

KalenderView accepts a Location from the timezone package. The KalenderEvent constructor automatically converts start and end to UTC, so events are always stored in UTC internally and converted to the given location for display.

import 'package:timezone/timezone.dart' as tz;

KalenderView(
  location: tz.getLocation('America/New_York'),
  eventsController: eventsController,
  kalenderController: kalenderController,
  viewConfiguration: viewConfiguration,
)

Pre-initialize DefaultEventsController with the locations you expect to query for best performance:

import 'package:timezone/timezone.dart' as tz;

final eventsController = DefaultEventsController(
  locations: [
    tz.getLocation('America/New_York'),
    tz.getLocation('Europe/London'),
    tz.getLocation('Asia/Tokyo'),
  ],
);

See the timezone package for setup instructions per platform. The web demo also provides a working example.

Changing location at runtime automatically updates visible date/time ranges. Location identifiers follow the IANA Time Zone Database.

Events from an external source

When events come from an .ics file, a device calendar, or an API, map each source time to the exact instant it represents before building the KalenderEvent. The constructor stores the instant as UTC, so what matters is that the DateTime you pass points at the right moment.

  • UTC instant (an .ics time ending in Z, or an epoch): pass it as-is.

  • Zoned time (an IANA TZID): build a TZDateTime in that zone so the instant is correct.

    import 'package:timezone/timezone.dart' as tz;
    
    final start = tz.TZDateTime(tz.getLocation('Europe/London'), 2025, 1, 6, 9);
    final event = KalenderEvent(
      start: start, end: start.add(const Duration(hours: 1)),
    );
    
  • Floating time (no zone, common in .ics): decide which zone it should mean, usually the calendar's location, and build a TZDateTime there.

Then set KalenderView(location:) to the zone the calendar should display in. The ics example shows this end to end.

Now Callback

By default, the time indicator position and "today" header highlighting are derived from the calendar's Location. If your app stores wall-clock times as UTC (e.g. an application where location: UTC) but still wants the indicator and today highlight to reflect the user's local time, pass a NowCallback on your view configuration:

MultiDayViewConfiguration.week(
  nowCallback: DateTime.now, // system local time
)

The callback's return value is used for:

  • Positioning the time indicator on the calendar grid.
  • Determining which day is "today" for header highlighting (DayHeader, MonthDayHeader, ScheduleDate).
  • Evaluating EmptyDayBehavior.showOnlyToday in schedule views.

Any DateTime subtype works, so the callback can return UTC or a TZDateTime in a specific zone.

nowCallback is included in the view configuration's equality, so pass the same function on every build. A tear-off such as DateTime.now is one, as is any top-level or static function. A closure works too, as long as it is stored rather than written inline:

import 'package:timezone/timezone.dart' as tz;

// Created once. Written inline it would be a new function every build, which
// recreates the view and drops its layout cache.
final nowInLondon = () => tz.TZDateTime.now(tz.getLocation('Europe/London'));

When nowCallback is null (the default), the calendar falls back to its Location-based behavior.

Classes

FloatingDateTime Dates and times
A date and time with no timezone, used for calendar layout.
FloatingDateTimeRange Dates and times
A range between two FloatingDateTimes, used for calendar layout.
KalenderDateTimeRange Dates and times
A range between two DateTimes.
KalenderTime Dates and times
A time of day, as an hour and a minute.
KalenderTimeRange Dates and times
Encapsulates a start and end KalenderTime that represents a day time range.